From cdb9e5b655fc6e7f4e9f340408e21231c943279c Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 7 Aug 2025 17:38:22 +0200 Subject: [PATCH] =?utf8?q?odhcpd:=20improve=20RFC9096=20=C2=A7=203.5=20SLA?= =?utf8?q?AC=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit When a CE router provides LAN-side address-configuration information via SLAAC: * A CE router sending RAs that advertise prefixes belonging to a dynamically learned prefix (e.g., via DHCPv6-PD) SHOULD record, on stable storage, the list of prefixes being advertised via PIOs on each network segment and the state of the "A" and "L" flags of the corresponding PIOs. * Upon changes to the advertised prefixes, and after bootstrapping, the CE router advertising prefix information via SLAAC proceeds as follows: - Any prefixes that were previously advertised by the CE router via PIOs in RA messages, but that have now become stale, MUST be advertised with PIOs that have the "Valid Lifetime" and the "Preferred Lifetime" set to 0 and the "A" and "L" bits unchanged. - The aforementioned advertisements MUST be performed for at least the "Valid Lifetime" previously employed for such prefixes. This should be enabled by default with a folder in "/tmp" to avoid forcing flash writes for all the users and at least handle the stale PIOs on PPPoe reconnections. For example: uci set dhcp.odhcpd.piofolder="/tmp/odhcpd-piofolder" uci commit dhcp There's a new ubus call which allows getting all the IPv6 RA PIOs, with information about their lifetime and stale status: ubus call dhcp ipv6ra { "interfaces": { "br-lan": [ { "lifetime": 5390, "prefix": "2001:db8:0:100::/64", "stale": true }, { "prefix": "2001:db8:0:200::/64", "stale": false }, { "prefix": "fd00::/64", "stale": false } ], "eth1.10": [ { "lifetime": 5390, "prefix": "2001:db8:0:110::/64", "stale": true }, { "prefix": "2001:db8:0:210::/64", "stale": false }, { "prefix": "fd00:0:0:10::/64", "stale": false } ] } } The current implementation performs a flash write per interface whenever a new prefix is added or an existing prefix becomes stale. Since some ISPs assign static or semi-static prefixes to their customers, it should be up to the user to enable flash writes when needed. Link: https://github.com/openwrt/odhcpd/pull/256 Signed-off-by: Álvaro Fernández Rojas --- CMakeLists.txt | 3 +- README | 2 + src/config.c | 341 ++++++++++++++++++++++++++++++++++++++++++++++++- src/netlink.c | 55 -------- src/odhcpd.h | 40 +++++- src/router.c | 187 +++++++++++++++++++++++---- src/ubus.c | 57 +++++++++ 7 files changed, 594 insertions(+), 91 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index de9a4e6..5690af3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ FIND_PATH(ubox_include_dir libubox/uloop.h) FIND_PATH(libnl-tiny_include_dir netlink-generic.h PATH_SUFFIXES libnl-tiny) INCLUDE_DIRECTORIES(${ubox_include_dir} ${libnl-tiny_include_dir}) +FIND_LIBRARY(json NAMES json-c) FIND_LIBRARY(libnl NAMES nl-tiny) add_definitions(-D_GNU_SOURCE -Os -Wall -Werror --std=gnu99) @@ -37,7 +38,7 @@ if(${DHCPV4_SUPPORT}) endif(${DHCPV4_SUPPORT}) add_executable(odhcpd src/odhcpd.c src/config.c src/router.c src/dhcpv6.c src/ndp.c src/dhcpv6-ia.c src/dhcpv6-pxe.c src/netlink.c ${EXT_SRC}) -target_link_libraries(odhcpd resolv ubox uci ${libnl} ${EXT_LINK}) +target_link_libraries(odhcpd resolv ubox uci ${json} ${libnl} ${EXT_LINK}) # Installation install(TARGETS odhcpd DESTINATION sbin/) diff --git a/README b/README index bb9c082..582d8ba 100644 --- a/README +++ b/README @@ -67,6 +67,8 @@ leasefile string DHCP/v6 lease/hostfile leasetrigger string Lease trigger script hostsfile string DHCP/v6 hostfile loglevel integer 6 Syslog level priority (0-7) +piofolder string Folder to store IPv6 prefix information (to + detect stale prefixes, see RFC9096, §3.5) Sections of type dhcp (configure DHCP / DHCPv6 / RA / NDP service) diff --git a/src/config.c b/src/config.c index a6b699e..72d3bde 100644 --- a/src/config.c +++ b/src/config.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -11,6 +12,7 @@ #include #include +#include #include #include #include @@ -29,9 +31,16 @@ static void lease_update(struct vlist_tree *tree, struct vlist_node *node_new, struct vlist_tree leases = VLIST_TREE_INIT(leases, lease_cmp, lease_update, true, false); AVL_TREE(interfaces, avl_strcmp, false, NULL); -struct config config = {.legacy = false, .main_dhcpv4 = false, - .dhcp_cb = NULL, .dhcp_statefile = NULL, .dhcp_hostsfile = NULL, - .log_level = LOG_WARNING}; +struct config config = { + .legacy = false, + .main_dhcpv4 = false, + .dhcp_cb = NULL, + .dhcp_statefile = NULL, + .dhcp_hostsfile = NULL, + .ra_piofolder = NULL, + .ra_piofolder_fd = -1, + .log_level = LOG_WARNING, +}; #define START_DEFAULT 100 #define LIMIT_DEFAULT 150 @@ -200,6 +209,7 @@ enum { ODHCPD_ATTR_LEASETRIGGER, ODHCPD_ATTR_LOGLEVEL, ODHCPD_ATTR_HOSTSFILE, + ODHCPD_ATTR_PIOFOLDER, ODHCPD_ATTR_MAX }; @@ -210,6 +220,7 @@ static const struct blobmsg_policy odhcpd_attrs[ODHCPD_ATTR_MAX] = { [ODHCPD_ATTR_LEASETRIGGER] = { .name = "leasetrigger", .type = BLOBMSG_TYPE_STRING }, [ODHCPD_ATTR_LOGLEVEL] = { .name = "loglevel", .type = BLOBMSG_TYPE_INT32 }, [ODHCPD_ATTR_HOSTSFILE] = { .name = "hostsfile", .type = BLOBMSG_TYPE_STRING }, + [ODHCPD_ATTR_PIOFOLDER] = { .name = "piofolder", .type = BLOBMSG_TYPE_STRING }, }; const struct uci_blob_param_list odhcpd_attr_list = { @@ -276,6 +287,7 @@ static void set_interface_defaults(struct interface *iface) iface->ra_mininterval = iface->ra_maxinterval/3; iface->ra_lifetime = -1; iface->ra_dns = true; + iface->pio_update = false; } static void clean_interface(struct interface *iface) @@ -318,7 +330,7 @@ static void close_interface(struct interface *iface) clean_interface(iface); free(iface->addr4); free(iface->addr6); - free(iface->invalid_addr6); + free(iface->pios); free(iface->ifname); free(iface); } @@ -389,6 +401,11 @@ static void set_config(struct uci_section *s) config.dhcp_hostsfile = strdup(blobmsg_get_string(c)); } + if ((c = tb[ODHCPD_ATTR_PIOFOLDER])) { + free(config.ra_piofolder); + config.ra_piofolder = strdup(blobmsg_get_string(c)); + } + if ((c = tb[ODHCPD_ATTR_LEASETRIGGER])) { free(config.dhcp_cb); config.dhcp_cb = strdup(blobmsg_get_string(c)); @@ -1480,6 +1497,8 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr } } + config_load_ra_pio(iface); + return 0; err: @@ -1703,6 +1722,309 @@ static int ipv6_pxe_from_uci(struct uci_section* s) return ipv6_pxe_entry_new(arch, url) ? -1 : 0; } +#define JSON_LENGTH "length" +#define JSON_PREFIX "prefix" +#define JSON_SLAAC "slaac" +#define JSON_TIME "time" + +static inline time_t config_time_from_json(time_t json_time) +{ + time_t ref, now; + + ref = time(NULL); + now = odhcpd_time(); + + if (now > json_time || ref > json_time) + return 0; + + return json_time + (now - ref); +} + +static inline time_t config_time_to_json(time_t config_time) +{ + time_t ref, now; + + ref = time(NULL); + now = odhcpd_time(); + + return config_time + (ref - now); +} + +static inline bool config_ra_pio_enabled(struct interface *iface) +{ + return config.ra_piofolder_fd >= 0 && iface->ra == MODE_SERVER && !iface->master; +} + +static bool config_ra_pio_time(json_object *slaac_json, time_t *slaac_time) +{ + time_t pio_json_time, pio_time; + json_object *time_json; + + time_json = json_object_object_get(slaac_json, JSON_TIME); + if (!time_json) + return true; + + pio_json_time = (time_t) json_object_get_int64(time_json); + if (!pio_json_time) + return true; + + pio_time = config_time_from_json(pio_json_time); + if (!pio_time) + return false; + + *slaac_time = pio_time; + + return true; +} + +static json_object *config_load_ra_pio_json(struct interface *iface) +{ + json_object *json; + int fd; + + fd = openat(config.ra_piofolder_fd, iface->ifname, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return NULL; + + json = json_object_from_fd(fd); + + close(fd); + + if (!json) + syslog(LOG_ERR, + "rfc9096: %s: json read error %s", + iface->ifname, + json_util_get_last_err()); + + return json; +} + +void config_load_ra_pio(struct interface *iface) +{ + json_object *json, *slaac_json; + size_t pio_cnt; + time_t now; + + if (!config_ra_pio_enabled(iface)) + return; + + json = config_load_ra_pio_json(iface); + if (!json) + return; + + slaac_json = json_object_object_get(json, JSON_SLAAC); + if (!slaac_json) { + json_object_put(json); + return; + } + + now = odhcpd_time(); + + pio_cnt = json_object_array_length(slaac_json); + iface->pios = malloc(sizeof(struct ra_pio) * pio_cnt); + if (!iface->pios) { + json_object_put(json); + return; + } + + iface->pio_cnt = 0; + for (size_t i = 0; i < pio_cnt; i++) { + json_object *cur_pio_json, *length_json, *prefix_json; + const char *pio_str; + time_t pio_lt = 0; + struct ra_pio *pio; + uint8_t pio_len; + + cur_pio_json = json_object_array_get_idx(slaac_json, i); + if (!cur_pio_json) + continue; + + if (!config_ra_pio_time(cur_pio_json, &pio_lt)) + continue; + + length_json = json_object_object_get(cur_pio_json, JSON_LENGTH); + if (!length_json) + continue; + + prefix_json = json_object_object_get(cur_pio_json, JSON_PREFIX); + if (!prefix_json) + continue; + + pio_len = (uint8_t) json_object_get_uint64(length_json); + pio_str = json_object_get_string(prefix_json); + pio = &iface->pios[iface->pio_cnt]; + + inet_pton(AF_INET6, pio_str, &pio->prefix); + pio->length = pio_len; + pio->lifetime = pio_lt; + syslog(LOG_INFO, + "rfc9096: %s: load %s/%u (%u)", + iface->ifname, + pio_str, + pio_len, + ra_pio_lifetime(pio, now)); + + iface->pio_cnt++; + } + + json_object_put(json); + + if (!iface->pio_cnt) { + free(iface->pios); + iface->pios = NULL; + } else if (iface->pio_cnt != pio_cnt) { + iface->pios = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt); + } +} + +static void config_save_ra_pio_json(struct interface *iface, struct json_object *json) +{ + size_t tmp_piofile_strlen; + char *tmp_piofile; + int fd, ret; + + tmp_piofile_strlen = strlen(iface->ifname) + 2; + tmp_piofile = alloca(tmp_piofile_strlen); + snprintf(tmp_piofile, tmp_piofile_strlen, ".%s", iface->ifname); + + fd = openat(config.ra_piofolder_fd, + tmp_piofile, + O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, + 0644); + if (fd < 0) { + syslog(LOG_ERR, + "rfc9096: %s: error %m creating temporary json file", + iface->ifname); + return; + } + + ret = json_object_to_fd(fd, json, JSON_C_TO_STRING_PLAIN); + if (ret) { + syslog(LOG_ERR, + "rfc9096: %s: json write error %s", + iface->ifname, + json_util_get_last_err()); + close(fd); + unlinkat(config.ra_piofolder_fd, tmp_piofile, 0); + return; + } + + ret = fsync(fd); + if (ret) { + syslog(LOG_ERR, + "rfc9096: %s: error %m syncing %s", + iface->ifname, + tmp_piofile); + close(fd); + unlinkat(config.ra_piofolder_fd, tmp_piofile, 0); + return; + } + + ret = close(fd); + if (ret) { + syslog(LOG_ERR, + "rfc9096: %s: error %m closing %s", + iface->ifname, + tmp_piofile); + unlinkat(config.ra_piofolder_fd, tmp_piofile, 0); + return; + } + + ret = renameat(config.ra_piofolder_fd, + tmp_piofile, + config.ra_piofolder_fd, + iface->ifname); + if (ret) { + syslog(LOG_ERR, + "rfc9096: %s: error %m renaming piofile: %s -> %s", + iface->ifname, + tmp_piofile, + iface->ifname); + close(fd); + unlinkat(config.ra_piofolder_fd, tmp_piofile, 0); + return; + } + + iface->pio_update = false; + syslog(LOG_WARNING, + "rfc9096: %s: piofile updated", + iface->ifname); +} + +void config_save_ra_pio(struct interface *iface) +{ + struct json_object *json, *slaac_json; + char ipv6_str[INET6_ADDRSTRLEN]; + time_t now; + + if (!config_ra_pio_enabled(iface)) + return; + + if (!iface->pio_update) + return; + + now = odhcpd_time(); + + json = json_object_new_object(); + if (!json) + return; + + slaac_json = json_object_new_array_ext(iface->pio_cnt); + if (!slaac_json) { + json_object_put(slaac_json); + return; + } + + json_object_object_add(json, JSON_SLAAC, slaac_json); + + for (size_t i = 0; i < iface->pio_cnt; i++) { + struct json_object *cur_pio_json, *len_json, *pfx_json; + const struct ra_pio *cur_pio = &iface->pios[i]; + + if (ra_pio_expired(cur_pio, now)) + continue; + + cur_pio_json = json_object_new_object(); + if (!cur_pio_json) + continue; + + inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str)); + + pfx_json = json_object_new_string(ipv6_str); + if (!pfx_json) { + json_object_put(cur_pio_json); + continue; + } + + len_json = json_object_new_uint64(cur_pio->length); + if (!len_json) { + json_object_put(cur_pio_json); + json_object_put(pfx_json); + continue; + } + + json_object_object_add(cur_pio_json, JSON_PREFIX, pfx_json); + json_object_object_add(cur_pio_json, JSON_LENGTH, len_json); + + if (cur_pio->lifetime) { + struct json_object *time_json; + time_t pio_lt; + + pio_lt = config_time_to_json(cur_pio->lifetime); + + time_json = json_object_new_int64(pio_lt); + if (time_json) + json_object_object_add(cur_pio_json, JSON_TIME, time_json); + } + + json_object_array_add(slaac_json, cur_pio_json); + } + + config_save_ra_pio_json(iface, json); + + json_object_put(json); +} + void odhcpd_reload(void) { struct uci_context *uci = uci_alloc_context(); @@ -1757,6 +2079,17 @@ void odhcpd_reload(void) free(path); } + if (config.ra_piofolder) { + char *path = strdupa(config.ra_piofolder); + + mkdir_p(path, 0755); + + close(config.ra_piofolder_fd); + config.ra_piofolder_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC); + if (config.ra_piofolder_fd < 0) + syslog(LOG_ERR, "Unable to open piofolder '%s': %m", path); + } + vlist_flush(&leases); #ifdef WITH_UBUS diff --git a/src/netlink.c b/src/netlink.c index 6b38caa..57cf566 100644 --- a/src/netlink.c +++ b/src/netlink.c @@ -210,61 +210,6 @@ static void refresh_iface_addr6(int ifindex) addr[i].valid_lt < iface->addr6[i].valid_lt || addr[i].preferred_lt < iface->addr6[i].preferred_lt) change = true; } - - if (change) { - /* - * Keep track of removed prefixes, so we could advertise them as invalid - * for at least a couple of times. - * - * RFC7084 § 4.3 : - * L-13: If the delegated prefix changes, i.e., the current prefix is - * replaced with a new prefix without any overlapping time - * period, then the IPv6 CE router MUST immediately advertise the - * old prefix with a Preferred Lifetime of zero and a Valid - * Lifetime of either a) zero or b) the lower of the current - * Valid Lifetime and two hours (which must be decremented in - * real time) in a Router Advertisement message as described in - * Section 5.5.3, (e) of [RFC4862]. - */ - - for (size_t i = 0; i < iface->addr6_len; ++i) { - bool removed = true; - - if (iface->addr6[i].valid_lt <= (uint32_t)now) - continue; - - for (ssize_t j = 0; removed && j < len; ++j) { - size_t plen = min(addr[j].prefix, iface->addr6[i].prefix); - - if (odhcpd_bmemcmp(&addr[j].addr.in6, &iface->addr6[i].addr.in6, plen) == 0) - removed = false; - } - - for (size_t j = 0; removed && j < iface->invalid_addr6_len; ++j) { - size_t plen = min(iface->invalid_addr6[j].prefix, iface->addr6[i].prefix); - - if (odhcpd_bmemcmp(&iface->invalid_addr6[j].addr.in6, &iface->addr6[i].addr.in6, plen) == 0) - removed = false; - } - - if (removed) { - size_t pos = iface->invalid_addr6_len; - struct odhcpd_ipaddr *new_invalid_addr6 = realloc(iface->invalid_addr6, - sizeof(*iface->invalid_addr6) * (pos + 1)); - - if (!new_invalid_addr6) - break; - - iface->invalid_addr6 = new_invalid_addr6; - iface->invalid_addr6_len++; - memcpy(&iface->invalid_addr6[pos], &iface->addr6[i], sizeof(*iface->invalid_addr6)); - iface->invalid_addr6[pos].valid_lt = iface->invalid_addr6[pos].preferred_lt = (uint32_t)now; - - if (iface->invalid_addr6[pos].prefix < 64) - iface->invalid_addr6[pos].prefix = 64; - } - } - } } iface->addr6 = addr; diff --git a/src/odhcpd.h b/src/odhcpd.h index 4c510e2..5b7b49f 100644 --- a/src/odhcpd.h +++ b/src/odhcpd.h @@ -140,7 +140,6 @@ struct odhcpd_ipaddr { /* ipv6 only */ struct { uint8_t dprefix; - uint8_t invalid_advertisements; bool tentative; }; @@ -173,6 +172,10 @@ struct config { char *dhcp_cb; char *dhcp_statefile; char *dhcp_hostsfile; + + char *ra_piofolder; + int ra_piofolder_fd; + int log_level; }; @@ -270,6 +273,14 @@ struct dnr_options { }; +// RA PIO - RFC9096 +struct ra_pio { + struct in6_addr prefix; + uint8_t length; + time_t lifetime; +}; + + struct interface { struct avl_node avl; @@ -281,8 +292,6 @@ struct interface { // IPv6 runtime data struct odhcpd_ipaddr *addr6; size_t addr6_len; - struct odhcpd_ipaddr *invalid_addr6; - size_t invalid_addr6_len; // RA runtime data struct odhcpd_event router_event; @@ -403,6 +412,11 @@ struct interface { // DNR struct dnr_options *dnr; size_t dnr_cnt; + + // RA PIO - RFC9096 + struct ra_pio *pios; + size_t pio_cnt; + bool pio_update; }; extern struct avl_tree interfaces; @@ -434,6 +448,24 @@ inline static struct dhcp_assignment *alloc_assignment(size_t extra_len) return a; } +inline static bool ra_pio_expired(const struct ra_pio *pio, time_t now) +{ + return pio->lifetime && (now > pio->lifetime); +} + +inline static uint32_t ra_pio_lifetime(const struct ra_pio *pio, time_t now) +{ + if (!pio->lifetime || now > pio->lifetime) + return 0; + + return (uint32_t) (pio->lifetime - now); +} + +inline static bool ra_pio_stale(const struct ra_pio *pio) +{ + return !!pio->lifetime; +} + // Exported main functions int odhcpd_register(struct odhcpd_event *event); int odhcpd_deregister(struct odhcpd_event *event); @@ -469,6 +501,8 @@ struct lease *config_find_lease_by_duid(const uint8_t *duid, const uint16_t len) struct lease *config_find_lease_by_mac(const uint8_t *mac); struct lease *config_find_lease_by_hostid(const uint64_t hostid); struct lease *config_find_lease_by_ipaddr(const uint32_t ipaddr); +void config_load_ra_pio(struct interface *iface); +void config_save_ra_pio(struct interface *iface); int set_lease_from_blobmsg(struct blob_attr *ba); #ifdef WITH_UBUS diff --git a/src/router.c b/src/router.c index 7982ccf..d8b6990 100644 --- a/src/router.c +++ b/src/router.c @@ -446,6 +446,116 @@ struct nd_opt_dnr_info { uint8_t body[]; }; +/* IPv6 RA PIOs */ +static struct ra_pio *router_find_ra_pio(struct interface *iface, + struct nd_opt_prefix_info *p) +{ + for (size_t i = 0; i < iface->pio_cnt; i++) { + struct ra_pio *cur_pio = &iface->pios[i]; + + if (p->nd_opt_pi_prefix_len == cur_pio->length && + !odhcpd_bmemcmp(&p->nd_opt_pi_prefix, &cur_pio->prefix, cur_pio->length)) + return cur_pio; + } + + return NULL; +} + +static void router_add_ra_pio(struct interface *iface, + struct nd_opt_prefix_info *p) +{ + char ipv6_str[INET6_ADDRSTRLEN]; + struct ra_pio *new_pios, *pio; + + pio = router_find_ra_pio(iface, p); + if (pio) { + if (pio->lifetime) { + pio->lifetime = 0; + + iface->pio_update = true; + syslog(LOG_WARNING, "rfc9096: %s: renew %s/%u", + iface->ifname, + inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)), + pio->length); + } + + return; + } + + new_pios = realloc(iface->pios, sizeof(struct ra_pio) * (iface->pio_cnt + 1)); + if (!new_pios) + return; + + iface->pios = new_pios; + pio = &iface->pios[iface->pio_cnt]; + iface->pio_cnt++; + + memcpy(&pio->prefix, &p->nd_opt_pi_prefix, sizeof(pio->prefix)); + pio->length = p->nd_opt_pi_prefix_len; + pio->lifetime = 0; + + iface->pio_update = true; + syslog(LOG_INFO, "rfc9096: %s: add %s/%u", + iface->ifname, + inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)), + pio->length); +} + +static void router_clear_ra_pio(time_t now, + struct interface *iface) +{ + size_t i = 0, pio_cnt = iface->pio_cnt; + char ipv6_str[INET6_ADDRSTRLEN]; + + while (i < iface->pio_cnt) { + struct ra_pio *cur_pio = &iface->pios[i]; + + if (ra_pio_expired(cur_pio, now)) { + syslog(LOG_INFO, + "rfc9096: %s: clear %s/%u", + iface->ifname, + inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str)), + cur_pio->length); + + if (i + 1 < iface->pio_cnt) + iface->pios[i] = iface->pios[iface->pio_cnt - 1]; + + iface->pio_cnt--; + } else { + i++; + } + } + + if (!iface->pio_cnt) { + free(iface->pios); + iface->pios = NULL; + } else if (iface->pio_cnt != pio_cnt) { + struct ra_pio *new_pios = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt); + + if (new_pios) + iface->pios = new_pios; + } +} + +static void router_stale_ra_pio(struct interface *iface, + struct nd_opt_prefix_info *p, + time_t now) +{ + struct ra_pio *pio = router_find_ra_pio(iface, p); + char ipv6_str[INET6_ADDRSTRLEN]; + + if (!pio || pio->lifetime) + return; + + pio->lifetime = now + iface->max_valid_lifetime; + + iface->pio_update = true; + syslog(LOG_WARNING, "rfc9096: %s: stale %s/%u", + iface->ifname, + inet_ntop(AF_INET6, &pio->prefix, ipv6_str, sizeof(ipv6_str)), + pio->length); +} + /* Router Advert server mode */ static int send_router_advert(struct interface *iface, const struct in6_addr *from) { @@ -463,7 +573,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr struct sockaddr_in6 dest; size_t dns_sz = 0, search_sz = 0, pref64_sz = 0, dnrs_sz = 0; size_t pfxs_cnt = 0, routes_cnt = 0; - size_t valid_addr_cnt = 0, invalid_addr_cnt = 0; + size_t total_addr_cnt = 0, valid_addr_cnt = 0; /* * lowest_found_lifetime stores the lowest lifetime of all prefixes; * necessary to find longest adv interval necessary @@ -475,6 +585,8 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr bool valid_prefix = false; char buf[INET6_ADDRSTRLEN]; + router_clear_ra_pio(now, iface); + memset(&adv, 0, sizeof(adv)); adv.h.nd_ra_type = ND_ROUTER_ADVERT; @@ -513,7 +625,6 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr iov[IOV_RA_ADV].iov_len = sizeof(adv); valid_addr_cnt = (iface->timer_rs.cb /* if not shutdown */ ? iface->addr6_len : 0); - invalid_addr_cnt = iface->invalid_addr6_len; // check ra_default if (iface->default_router) { @@ -523,47 +634,46 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr valid_prefix = true; } - if (valid_addr_cnt + invalid_addr_cnt) { - addrs = alloca(sizeof(*addrs) * (valid_addr_cnt + invalid_addr_cnt)); + if (valid_addr_cnt + iface->pio_cnt) { + addrs = alloca(sizeof(*addrs) * (valid_addr_cnt + iface->pio_cnt)); if (valid_addr_cnt) { memcpy(addrs, iface->addr6, sizeof(*addrs) * valid_addr_cnt); + total_addr_cnt = valid_addr_cnt; /* Check default route */ if (!default_route && parse_routes(addrs, valid_addr_cnt)) default_route = true; } - if (invalid_addr_cnt) { - size_t i = 0; + for (size_t i = 0; i < iface->pio_cnt; i++) { + struct ra_pio *cur_pio = &iface->pios[i]; + bool pio_found = false; - memcpy(&addrs[valid_addr_cnt], iface->invalid_addr6, sizeof(*addrs) * invalid_addr_cnt); + for (size_t j = 0; j < valid_addr_cnt; j++) { + struct odhcpd_ipaddr *cur_addr = &addrs[j]; - /* Remove invalid prefixes that were advertised 3 times */ - while (i < iface->invalid_addr6_len) { - if (++iface->invalid_addr6[i].invalid_advertisements >= 3) { - if (i + 1 < iface->invalid_addr6_len) - memmove(&iface->invalid_addr6[i], &iface->invalid_addr6[i + 1], sizeof(*addrs) * (iface->invalid_addr6_len - i - 1)); - - iface->invalid_addr6_len--; + if (cur_pio->length == cur_addr->prefix && + !odhcpd_bmemcmp(&cur_pio->prefix, &cur_addr->addr.in6, cur_pio->length)) { + pio_found = true; + break; + } + } - if (iface->invalid_addr6_len) { - struct odhcpd_ipaddr *new_invalid_addr6 = realloc(iface->invalid_addr6, sizeof(*addrs) * iface->invalid_addr6_len); + if (!pio_found) { + struct odhcpd_ipaddr *addr = &addrs[total_addr_cnt]; - if (new_invalid_addr6) - iface->invalid_addr6 = new_invalid_addr6; - } else { - free(iface->invalid_addr6); - iface->invalid_addr6 = NULL; - } - } else - ++i; + memcpy(&addr->addr.in6, &cur_pio->prefix, sizeof(addr->addr.in6)); + addr->prefix = cur_pio->length; + addr->preferred_lt = 0; + addr->valid_lt = (uint32_t) (now + ND_VALID_LIMIT); + total_addr_cnt++; } } } /* Construct Prefix Information options */ - for (size_t i = 0; i < valid_addr_cnt + invalid_addr_cnt; ++i) { + for (size_t i = 0; i < total_addr_cnt; ++i) { struct odhcpd_ipaddr *addr = &addrs[i]; struct nd_opt_prefix_info *p = NULL; uint32_t preferred_lt = 0; @@ -651,8 +761,26 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO; if (iface->ra_advrouter) p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_RADDR; - p->nd_opt_pi_preferred_time = htonl(preferred_lt); - p->nd_opt_pi_valid_time = htonl(valid_lt); + if (i >= valid_addr_cnt || !preferred_lt) { + /* + * RFC9096 § 3.5 + * + * - Any prefixes that were previously advertised by the CE router + * via PIOs in RA messages, but that have now become stale, MUST + * be advertised with PIOs that have the "Valid Lifetime" and the + * "Preferred Lifetime" set to 0 and the "A" and "L" bits + * unchanged. + */ + p->nd_opt_pi_preferred_time = 0; + p->nd_opt_pi_valid_time = 0; + + router_stale_ra_pio(iface, p, now); + } else { + p->nd_opt_pi_preferred_time = htonl(preferred_lt); + p->nd_opt_pi_valid_time = htonl(valid_lt); + + router_add_ra_pio(iface, p); + } } iov[IOV_RA_PFXS].iov_base = (char *)pfxs; @@ -892,9 +1020,12 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr syslog(LOG_NOTICE, "Sending a RA on %s", iface->name); - if (odhcpd_send(iface->router_event.uloop.fd, &dest, iov, ARRAY_SIZE(iov), iface) > 0) + if (odhcpd_send(iface->router_event.uloop.fd, &dest, iov, ARRAY_SIZE(iov), iface) > 0) { iface->ra_sent++; + config_save_ra_pio(iface); + } + out: free(pfxs); free(routes); diff --git a/src/ubus.c b/src/ubus.c index be3deac..36d7dbb 100644 --- a/src/ubus.c +++ b/src/ubus.c @@ -176,6 +176,62 @@ static int handle_dhcpv6_leases(_unused struct ubus_context *ctx, _unused struct return 0; } +static int handle_ra_pio(_unused struct ubus_context *ctx, _unused struct ubus_object *obj, + _unused struct ubus_request_data *req, _unused const char *method, + _unused struct blob_attr *msg) +{ + char ipv6_str[INET6_ADDRSTRLEN]; + time_t now = odhcpd_time(); + struct interface *iface; + void *interfaces_blob; + + blob_buf_init(&b, 0); + + interfaces_blob = blobmsg_open_table(&b, "interfaces"); + + avl_for_each_element(&interfaces, iface, avl) { + void *interface_blob; + + if (iface->ra != MODE_SERVER) + continue; + + interface_blob = blobmsg_open_array(&b, iface->ifname); + + for (size_t i = 0; i < iface->pio_cnt; i++) { + struct ra_pio *cur_pio = &iface->pios[i]; + void *cur_pio_blob; + uint32_t pio_lt; + bool pio_stale; + + if (ra_pio_expired(cur_pio, now)) + continue; + + cur_pio_blob = blobmsg_open_table(&b, NULL); + + pio_lt = ra_pio_lifetime(cur_pio, now); + pio_stale = ra_pio_stale(cur_pio); + + inet_ntop(AF_INET6, &cur_pio->prefix, ipv6_str, sizeof(ipv6_str)); + + if (pio_lt) + blobmsg_add_u32(&b, "lifetime", pio_lt); + blobmsg_add_string(&b, "prefix", ipv6_str); + blobmsg_add_u16(&b, "length", cur_pio->length); + blobmsg_add_u8(&b, "stale", pio_stale); + + blobmsg_close_table(&b, cur_pio_blob); + } + + blobmsg_close_array(&b, interface_blob); + } + + blobmsg_close_table(&b, interfaces_blob); + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + static int handle_add_lease(_unused struct ubus_context *ctx, _unused struct ubus_object *obj, _unused struct ubus_request_data *req, _unused const char *method, struct blob_attr *msg) @@ -189,6 +245,7 @@ static int handle_add_lease(_unused struct ubus_context *ctx, _unused struct ubu static struct ubus_method main_object_methods[] = { {.name = "ipv4leases", .handler = handle_dhcpv4_leases}, {.name = "ipv6leases", .handler = handle_dhcpv6_leases}, + {.name = "ipv6ra", .handler = handle_ra_pio}, UBUS_METHOD("add_lease", handle_add_lease, lease_attrs), }; -- 2.30.2